/*
 * ComponentRegistryReader.java
 *
 * Created on February 26, 2007, 11:34 AM
 *
 * (C) R. Alexander Milowski alex@milowski.com
 */

package org.infoset.component;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
import org.infoset.xml.Document;
import org.infoset.xml.DocumentLoader;
import org.infoset.xml.Element;
import org.infoset.xml.InfosetFactory;
import org.infoset.xml.Name;
import org.infoset.xml.XMLException;
import org.infoset.xml.sax.SAXDocumentLoader;

/**
 *
 * @author R. Alexander Milowski
 */
public class ComponentRegistryReader {
   
   static URI NAMESPACE = URI.create("http://www.infoset.org/Vocabulary/XProc/ComponentRegistry/2007/1/0");
   static Name COMPONENT_REGISTRY_NAME = InfosetFactory.createName(NAMESPACE,"component-registry");
   static Name COMPONENT_NAME = InfosetFactory.createName(NAMESPACE,"component");
   static Logger LOG = Logger.getLogger(ComponentRegistryReader.class.getName());
   static ComponentRegistryReader instance = new ComponentRegistryReader();
   
   public static ComponentRegistryReader getInstance() {
      return instance;
   }
   
   static class InternalComponentRegistry extends HashMap<Name,ComponentDefinition> implements ComponentRegistry {
      Name name;
      List<ComponentRegistry> subordinate;
      
      InternalComponentRegistry(Name name) {
         this.name = name;
         this.subordinate = new ArrayList<ComponentRegistry>();
      }
      
      public Name getName() {
         return name;
      }
      
      public List<ComponentRegistry> getSubordinate() {
         return subordinate;
      }
      
      public ComponentDefinition get(Object key) {
         if (!(key instanceof Name)) {
            return null;
         }
         Name name = (Name)key;
         ComponentDefinition def = super.get(name);
         if (def==null) {
            for (ComponentRegistry reg : subordinate) {
               def = reg.get(name);
               if (def!=null) {
                  break;
               }
            }
         }
         return def;
      }
      
      public Set<Name> keySet() {
         if (subordinate.size()==0) {
            return super.keySet();
         } else {
            HashSet<Name> names = new HashSet();
            names.addAll(super.keySet());
            for (ComponentRegistry reg : subordinate) {
               names.addAll(reg.keySet());
            }
            return Collections.unmodifiableSet(names);
         }
      }
      
      public Collection<ComponentDefinition> values() {
         if (subordinate.size()==0) {
            return super.values();
         } else {
            List<ComponentDefinition> values = new ArrayList<ComponentDefinition>();
            values.addAll(super.values());
            for (ComponentRegistry reg : subordinate) {
               for (Name name : reg.keySet()) {
                  if (super.get(name)==null) {
                     values.add(reg.get(name));
                  }
               }
            }
            return Collections.unmodifiableList(values);
         }
      }
   }
   
   static class ComponentDefinitionWrapper implements ComponentDefinition {
      Name name;
      String version;
      String vendor;
      URI location;
      ComponentDefinition actualDef;
      
      ComponentDefinitionWrapper(Name name,String version,String vendor, URI location,ComponentDefinition actualDef) {
         this.name = name;
         this.actualDef = actualDef;
         this.version = version==null ? actualDef.getVersion() : version;
         this.vendor = vendor==null ? actualDef.getVendor() : vendor;
         this.location = location==null ? actualDef.getVendorLocation() : location;
      }
      
      public Component newInstance()
         throws XMLException 
      {
         return actualDef.newInstance();
      }

      public Name getName() {
         return name;
      }

      public String getVersion() {
         return version;
      }

      public String getVendor() {
         return vendor;
      }
      
      public URI getVendorLocation() {
         return location;
      }

      public Collection<Name> getInputs() {
         return actualDef.getInputs();
      }

      public Collection<Name> getOutputs() {
         return actualDef.getOutputs();
      }
   }
   

   ClassLoader classLoader;
   DocumentLoader docLoader;
   /** Creates a new instance of ComponentRegistryReader */
   public ComponentRegistryReader() {
      classLoader = this.getClass().getClassLoader();
      docLoader = new SAXDocumentLoader();
   }
   
   public ComponentRegistryReader(ClassLoader loader,DocumentLoader docLoader) {
      classLoader = loader;
      docLoader = docLoader;
   }
   
   public ComponentRegistry newRegistry(Name name) {
      return new InternalComponentRegistry(name);
   }
   
   
   public ComponentRegistry read(URI location)
      throws XMLException
   {
      try {
         DocumentLoader loader = new SAXDocumentLoader();
         Document registryDoc = loader.load(location);
         Element top = registryDoc.getDocumentElement();
         if (!top.getName().equals(COMPONENT_REGISTRY_NAME)) {
            throw new XMLException("Unrecongized registry element name "+top.getName());
         }
         String qname = top.getAttributeValue("name");
         if (qname==null) {
            throw new XMLException("The 'name' attribute is required on "+COMPONENT_REGISTRY_NAME);
         }
         Name registryName = top.getNamespaceScope().expandName(qname);
         ComponentRegistry registry = newRegistry(registryName);
         loadComponents(registry,top);
         return registry;
      } catch (IOException ex) {
         throw new XMLException("Cannot load registry from "+location+" due to I/O error.",ex);
      }
   }
   
   public void loadComponents(ComponentRegistry registry,Element registryTop)
      throws XMLException
   {
      String globalVersion = registryTop.getAttributeValue("version");
      String globalVendor = registryTop.getAttributeValue("vendor");
      String sglobalLocation = registryTop.getAttributeValue("location");
      URI globalLocation = null;
      if (sglobalLocation!=null) {
         try {
            globalLocation = new URI(sglobalLocation);
         } catch (URISyntaxException ex) {
            throw new XMLException("Bad registry vendor location '"+sglobalLocation+"': "+ex.getMessage() ,ex);
         }
      }
      for (Iterator<Element> componentDefs = registryTop.getElementsByName(COMPONENT_NAME);
           componentDefs.hasNext(); ) {
         Element defElement = componentDefs.next();
         String qname = defElement.getAttributeValue("name");
         if (qname==null) {
            throw new XMLException("The 'name' attribute is required on "+COMPONENT_NAME);
         }
         Name componentName = defElement.getNamespaceScope().expandName(qname);
         String vendor = defElement.getAttributeValue("vendor");
         if (vendor==null) {
            vendor = globalVendor;
         }
         String version = defElement.getAttributeValue("version");
         if (version==null) {
            version = globalVersion;
         }
         String slocation = defElement.getAttributeValue("location");
         URI location = null;
         try {
            location = slocation==null ? globalLocation : new URI(slocation);
         } catch (URISyntaxException ex) {
            throw new XMLException("Bad registry vendor location '"+slocation+"': "+ex.getMessage() ,ex);
         }
         String className = defElement.getAttributeValue("class");
         if (className==null) {
            throw new XMLException("The 'class' attribute is required on "+COMPONENT_NAME);
         }
         try {
            Class defClass = classLoader==null ? Class.forName(className) : classLoader.loadClass(className);
            Constructor defConstructor = null;
            ComponentDefinition def = null;
            boolean wrap = false;
            try {
               defConstructor = defClass.getConstructor(new Class[] { Name.class, String.class, String.class, URI.class });
               def = (ComponentDefinition)defConstructor.newInstance(new Object [] { componentName, vendor, version, location });
            } catch (NoSuchMethodException ex) {
            } catch (Exception ex) {
               throw new XMLException("Cannot instantiate class "+className+" for component "+componentName);
            }
            if (defConstructor==null) {
               wrap = true;
               try {
                  defConstructor = defClass.getConstructor(new Class[] { Name.class });
                  def = (ComponentDefinition)defConstructor.newInstance(new Object [] { componentName });
               } catch (NoSuchMethodException ex) {
               } catch (Exception ex) {
                  throw new XMLException("Cannot instantiate class "+className+" for component "+componentName);
               }
            }
            if (defConstructor==null) {
               wrap = true;
               try {
                  defConstructor = defClass.getConstructor(new Class [] {} );
                  def = (ComponentDefinition)defConstructor.newInstance();
               } catch (NoSuchMethodException ex) {
                  throw new XMLException("No constructor can be found for component definition "+componentName+" on class "+className);
               } catch (Exception ex) {
                  throw new XMLException("Cannot instantiate class "+className+" for component "+componentName);
               }
            }
            if (def==null) {
               throw new XMLException("No constructor can be found for component definition "+componentName+" on class "+className);
            }
            LOG.fine("Loaded component "+componentName+" -> "+className);
            registry.put(componentName,wrap ? new ComponentDefinitionWrapper(componentName,version,vendor,location,def) : def);
         } catch (ClassNotFoundException ex) {
            throw new XMLException("Cannot load component definition "+componentName+" class due to class "+ex.getMessage(),ex);
         }
      }
      for (Iterator<Element> regs = registryTop.getElementsByName(COMPONENT_REGISTRY_NAME);
           regs.hasNext(); ) {
         Element reg = regs.next();
         String href = reg.getAttributeValue("href");
         String resource = reg.getAttributeValue("resource");
         if (href==null && resource==null) {
            continue;
         }
         URI loc = null;
         if (href!=null) {
            URI base = reg.getBaseURI();
            try {
               loc = base!=null ? base.resolve(href) : new URI(href);
            } catch (URISyntaxException ex) {
               throw new XMLException("Cannot build URI from relative value: "+href,ex);
            }
         } else if (resource!=null) {
            URL classLoc = this.getClass().getResource(resource);
            if (classLoc==null) {
               throw new XMLException("Cannot find resource "+resource);
            }
            try {
               loc = classLoc.toURI();
            } catch (URISyntaxException ex) {
               throw new XMLException("Cannot build URI from resource URL: "+classLoc,ex);
            }
         }
         registry.getSubordinate().add(read(loc));
      }
      
   }
   
}
